<!DOCTYPE html>
<html lang="utf-8">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <title>Graphviz Online</title>
  <style type="text/css" media="screen">
    body {
      overflow: hidden;
      margin: 0 0;
    }

    body.presentation #editor {
      display: none;
    }

    body.presentation #options {
      left: 0;
    }

    body.presentation #review {
      left: 0;
    }

    #editor {
      margin: 0;
      position: absolute;
      top: 0;
      bottom: 0;
      right: 50%;
      left: 0;
    }

    #review {
      margin: 0;
      position: absolute;
      top: 50px;
      bottom: 0;
      right: 0;
      left: 50%;
      overflow: scroll;
    }

    #options {
      margin: 0;
      position: fixed;
      left: 50%;
      width: 100%;
    }

    #options {
      flex: 0 0 auto;
      background: #eee;
      border-bottom: 1px solid #ccc;
      padding: 8px;
      overflow: hidden;
    }

    #options label {
      margin-right: 8px;
    }

    #options #raw.disabled {
      opacity: 0.5;
    }

    #shareurl {
      display: none;
    }

    #status {
      width: 100%;
      position: fixed;
      bottom: 0;
      display: block;
      color: #FFF;
      z-index: 999;
    }

    #review svg {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }

    #review #text {
      font-size: 12px;
      font-family: monaco, courier, monospace;
      white-space: pre;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      overflow: auto;
    }

    #review img {
      display: block;
      margin: 0 auto;
    }

    #review.working svg,
    #output.error svg,
    #review.working #text,
    #output.error #text,
    #review.working img,
    #output.error img {
      opacity: 0.4;
    }

    #review.error #error {
      display: inherit;
    }

    #review #error {
      display: none;
      position: absolute;
      top: 20px;
      left: 20px;
      margin-right: 20px;
      background: red;
      color: white;
      z-index: 1;
    }

    #download {
      font: bold 12px Arial;
      text-decoration: none;
      background-color: #EEEEEE;
      color: #333333;
      padding: 2px 6px 2px 6px;
      border-top: 1px solid #CCCCCC;
      border-right: 1px solid #333333;
      border-bottom: 1px solid #333333;
      border-left: 1px solid #CCCCCC;
    }
  </style>
</head>

<body>
  <pre id="editor">digraph G {

  subgraph cluster_0 {
    style=filled;
    color=lightgrey;
    node [style=filled,color=white];
    a0 -> a1 -> a2 -> a3;
    label = "process #1";
  }

  subgraph cluster_1 {
    node [style=filled];
    b0 -> b1 -> b2 -> b3;
    label = "process #2";
    color=blue
  }
  start -> a0;
  start -> b0;
  a1 -> b3;
  b2 -> a3;
  a3 -> a0;
  a3 -> end;
  b3 -> end;

  start [shape=Mdiamond];
  end [shape=Msquare];
}</pre>
  <div id="options">
    <label id="engine">
      Engine:
      <select>
        <option>circo</option>
        <option selected="">dot</option>
        <option>fdp</option>
        <option>nop</option>
        <option>nop2</option>
        <option>neato</option>
        <option>sfdp</option>
        <option>twopi</option>
        <option>osage</option>
        <option>patchwork</option>
      </select>
    </label>

    <label id="format">
      Format:
      <select>
        <option selected="">svg</option>
        <option>png</option>
        <option>json</option>
        <option>xdot</option>
        <option>plain</option>
        <option>ps</option>
      </select>
    </label>

    <label id="raw" class="disabled">
      <input type="checkbox" disabled=""> Show raw output
    </label>

    <label>
      <a href="#" target="_blank" id="download">Download</a>
    </label>

    <label>
      <input type="button" value="Share" id="share">
    </label>

    <input type="input" value="" id="shareurl">

  </div>
  <div id="review">
    <div id="error"></div>
  </div>
  <div id="status"></div>
  <div class="github-fork-ribbon-wrapper right">
    <div class="github-fork-ribbon">
      <a href="https://github.com/dreampuf/graphvizonline">Fork me on GitHub</a>
    </div>
  </div>

  <script src="ace/ace.js" type="text/javascript" charset="utf-8"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js"
    integrity="sha512-qtX0GLM3qX8rxJN1gyDfcnMFFrKvixfoEOwbBib9VafR5vbChV5LeE5wSI/x+IlCkTY5ZFddFDCCfaVJJNnuKQ=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <script src="viz-standalone.js"></script>
  <script src="svg-pan-zoom.min.js" type="text/javascript" charset="utf-8"></script>
  <script>
    (function (document) {
      //http://stackoverflow.com/a/10372280/398634
      window.URL = window.URL || window.webkitURL;
      var el_stetus = document.getElementById("status"),
        t_stetus = -1,
        reviewer = document.getElementById("review"),
        scale = window.devicePixelRatio || 1,
        downloadBtn = document.getElementById("download"),
        editor = ace.edit("editor"),
        lastHD = -1,
        worker = null,
        parser = new DOMParser(),
        showError = null,
        formatEl = document.querySelector("#format select"),
        engineEl = document.querySelector("#engine select"),
        rawEl = document.querySelector("#raw input"),
        shareEl = document.querySelector("#share"),
        shareURLEl = document.querySelector("#shareurl"),
        errorEl = document.querySelector("#error");

      function show_status(text, hide) {
        hide = hide || 0;
        clearTimeout(t_stetus);
        el_stetus.innerHTML = text;
        if (hide) {
          t_stetus = setTimeout(function () {
            el_stetus.innerHTML = "";
          }, hide);
        }
      }

      function show_error(e) {
        console.trace();
        show_status("error", 500);
        reviewer.classList.remove("working");
        reviewer.classList.add("error");
        var message = e.message === undefined ? "An error occurred while processing the graph input." : e.message;
        while (errorEl.firstChild) {
          errorEl.removeChild(errorEl.firstChild);
        }
        errorEl.appendChild(document.createTextNode(message));
      }

      function svgXmlToImage(svgXml, callback) {
        var pngImage = new Image(), svgImage = new Image();

        svgImage.onload = function () {
          var canvas = document.createElement("canvas");
          canvas.width = svgImage.width * scale;
          canvas.height = svgImage.height * scale;

          var context = canvas.getContext("2d");
          context.drawImage(svgImage, 0, 0, canvas.width, canvas.height);

          pngImage.src = canvas.toDataURL("image/png");
          pngImage.width = svgImage.width;
          pngImage.height = svgImage.height;

          if (callback !== undefined) {
            callback(null, pngImage);
          }
        }

        svgImage.onerror = function (e) {
          if (callback !== undefined) {
            callback(e);
          }
        }
        svgImage.src = svgXml;
      }

      function copyShareURL(e) {
        let content = encodeURIComponent(editor.getSession().getDocument().getValue());
        const longUrl = new URL(location.href);
        longUrl.hash = content;

        shareEl.disabled = true;
        let n = 0;
        let animateId = setInterval(()=> { shareEl.value = "Loading" + ".".repeat(n++%3)}, 300)
        // cors for is.gd
        fetch("https://api.allorigins.win/get?url=" + encodeURIComponent("https://is.gd/create.php?" + new URLSearchParams({
          format: 'simple',
          url: longUrl.toString(),
        }).toString()))
          .then((r) => {
            if (r.ok) return r.json()
            return new Error("network issues");
          })
          .then((rs) => {
            shareURLEl.style.display = "inline";
            shareURLEl.value = rs.contents;
          }).catch((err) => {
            const compressedContent = LZString.compressToEncodedURIComponent(content);
            const compressedUrl = new URL(location.href);
            compressedUrl.searchParams.append("compressed", compressedContent);
            compressedUrl.hash = "";
            shareURLEl.style.display = "inline";
            shareURLEl.value = compressedUrl.toString();
          }).finally(()=>{
            clearInterval(animateId);
            shareEl.value = "Share";
            shareEl.disabled = false;
          })
      }

      function copyToClipboard(str) {
        const el = document.createElement('textarea');
        el.value = str;
        el.setAttribute('readonly', '');
        el.style.position = 'absolute';
        el.style.left = '-9999px';
        document.body.appendChild(el);
        const selected =
          document.getSelection().rangeCount > 0
            ? document.getSelection().getRangeAt(0)
            : false;
        el.select();
        var result = document.execCommand('copy')
        document.body.removeChild(el);
        if (selected) {
          document.getSelection().removeAllRanges();
          document.getSelection().addRange(selected);
        }
        return result;
      };

      function renderGraph() {
        reviewer.classList.add("working");
        reviewer.classList.remove("error");

        show_status("rendering...");
        Viz.instance().then(function (viz) {
          let dotContent = editor.getSession().getDocument().getValue();
          let options = {
            format: formatEl.value,
            engine: engineEl.value,
          };
          let result = null;

          if (["svg", "png"].indexOf(formatEl.value) > -1) {
            result = viz.renderSVGElement(dotContent, options);
          } else {
            result = viz.render(dotContent, options);
          }

          if (result.status !== undefined && result.status != "success") {
            show_error(result.errors && result.errors.length > 0 && result.errors[0] || result);
          } else {
            updateOutput(result);
          }
        }).catch((err) => {
          show_error(err);
        }).finally(() => {
          reviewer.classList.remove("working");
          show_status("done", 500)
        });
      }

      function updateState() {
        const updatedUrl = new URL(window.location)
        // Hash
        const content = encodeURIComponent(editor.getSession().getDocument().getValue());
        updatedUrl.hash = content
        // Search params
        updatedUrl.searchParams.set("engine", engineEl.value)
        history.pushState({ "content": content, "engine": engineEl.value }, "", updatedUrl.toString())
      }

      function updateOutput(result) {
        if (formatEl.value === "svg") {
          document.querySelector("#raw").classList.remove("disabled");
          rawEl.disabled = false;
        } else {
          document.querySelector("#raw").classList.add("disabled");
          rawEl.disabled = true;
        }

        var text = reviewer.querySelector("#text");
        if (text) {
          reviewer.removeChild(text);
        }

        var a = reviewer.querySelector("a");
        if (a) {
          reviewer.removeChild(a);
        }

        if (!result) {
          return;
        }

        reviewer.classList.remove("working");
        reviewer.classList.remove("error");

        if (formatEl.value == "svg" && !rawEl.checked) {
          var serializer = new XMLSerializer();
          var source = serializer.serializeToString(result);
          // https://stackoverflow.com/questions/18925210/download-blob-content-using-specified-charset
          //const blob = new Blob(["\ufeff", svg], {type: 'image/svg+xml;charset=utf-8'});
          const url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
          downloadBtn.href = url;
          downloadBtn.download = "graphviz.svg";
          var a = document.createElement("a");
          a.appendChild(result);
          reviewer.appendChild(a);
          svgPanZoom(result, {
            zoomEnabled: true,
            controlIconsEnabled: true,
            fit: true,
            center: true,
          });
        } else if (formatEl.value == "png") {
          var serializer = new XMLSerializer();
          var source = serializer.serializeToString(result);
          let resultWithPNGHeader = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(source)));
          svgXmlToImage(resultWithPNGHeader, function (err, image) {
            if (err) {
              show_error(err)
              return
            }
            image.setAttribute("title", "graphviz");
            downloadBtn.href = image.src;
            downloadBtn.download = "graphviz.png";
            var a = document.createElement("a");
            a.appendChild(image);
            reviewer.appendChild(a);
          })
        } else {
          var text = document.createElement("div");
          text.id = "text";
          if (formatEl.value == "svg") {
            let serializer = new XMLSerializer();
            result = serializer.serializeToString(result);
          } else {
            result = result.output;
          }
          text.appendChild(document.createTextNode(result));
          reviewer.appendChild(text);
        }

        updateState()
      }

      editor.setTheme("ace/theme/twilight");
      editor.getSession().setMode("ace/mode/dot");
      editor.getSession().on("change", function () {
        clearTimeout(lastHD);
        lastHD = setTimeout(renderGraph, 1500);
      });

      window.onpopstate = function (event) {
        if (event.state != null && event.state.content != undefined) {
          editor.getSession().setValue(decodeURIComponent(event.state.content));
        }
      };

      formatEl.addEventListener("change", renderGraph);
      engineEl.addEventListener("change", renderGraph);
      rawEl.addEventListener("change", renderGraph);
      share.addEventListener("click", copyShareURL);

      // Since apparently HTMLCollection does not implement the oh so convenient array functions
      HTMLOptionsCollection.prototype.indexOf = function (name) {
        for (let i = 0; i < this.length; i++) {
          if (this[i].value == name) {
            return i;
          }
        }

        return -1;
      };

      /* parsing from URL sharing */
      const params = new URLSearchParams(location.search.substring(1));
      if (params.has('engine')) {
        const engine = params.get('engine');
        const index = engineEl.options.indexOf(engine);
        if (index > -1) { // if index exists
          engineEl.selectedIndex = index;
        } else {
          show_error({ message: `invalid engine ${engine} selected` });
        }
      }

      if (params.has("presentation")) {
        document.body.classList.add("presentation");
      }

      if (params.has('raw')) {
        editor.getSession().setValue(params.get('raw'));
        renderGraph();
      } else if (params.has('compressed')) {
        const compressed = params.get('compressed');
        editor.getSession().setValue(LZString.decompressFromEncodedURIComponent(compressed));
      } else if (params.has('url')) {
        const url = params.get('url');
        let ok = false;
        fetch(url)
          .then(res => {
            ok = res.ok;
            return res.text();
          })
          .then(res => {
            if (!ok) {
              throw { message: res };
            }

            editor.getSession().setValue(res);
            renderGraph();
          }).catch(e => {
            show_error(e);
          });
      } else if (location.hash.length > 1) {
        editor.getSession().setValue(decodeURIComponent(location.hash.substring(1)));
      } else if (editor.getValue()) { // Init
        renderGraph();
      }

    })(document);
  </script>
  <iframe id="frame1" name="frame1" src="https://is.gd/create.php" frameborder="0"></iframe>
</body>

</html>